Skip to content

fix(plugin-detail): the summary chip beside the H1 draws an object value through its own cell renderer - #8732

Merged
os-justin merged 4 commits into
mainfrom
claude/issue-8464-summary-chip-object-value
Sep 9, 2026
Merged

fix(plugin-detail): the summary chip beside the H1 draws an object value through its own cell renderer#8732
os-justin merged 4 commits into
mainfrom
claude/issue-8464-summary-chip-object-value

Conversation

@os-justin

@os-justin os-justin commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8464

The defect, reproduced on this branch's base

⭐ Leg 1. The card was filed from another dev's measurement on a different branch, so it was re-measured here — DetailView rendered on ed971e8fc with summaryFields: ['owner_ref','billing_address','office_location'] and three object values:

chip text aria-label
owner_ref (expanded lookup {id,name}) [object Object] owner_ref: [object Object]
billing_address ({street,city,…}) [object Object] billing_address: [object Object]
office_location ({latitude,longitude}) [object Object] office_location: [object Object]

H1 read Acme Corporation, so all three pills sat beside a correctly-rendered title. Both halves of the card's claim hold: the text and the accessible name carry the placeholder, because the chip builds one from the other.

Which question the chip asks — measured, not argued

objectui#8395 established on this page that "render what the user sees" and "render the underlying value" give different answers per field type, and that the intuitive choice was wrong for 9 of 17. That table is about a clipboard payload. Re-derived against today's tree (packages/plugin-detail/src/__tests__/DetailSection.copyObjectValues-8395.test.tsx still pins it): date renders Mar 4 against a stored 2026-03-04, percent renders 12% against 0.123, image and boolean render no text at all — which is why a payload must take the stored value.

The chip is not a payload. Its own existing behaviour answers which side it is on, measured here on ed971e8fc:

field stored the chip printed
currency 1234.5 $1,235
date '2026-03-04' Mar 4, 2026
datetime '2024-07-04T07:00:00.000Z' Jul 4, 2024, 7:00 AM
select 'won' Closed Won

Every one is the seen face, never the stored one. So the display authority for a kind the chip does not itself format is that kind's own cell renderer — option A, the route HeaderHighlight already takes one band below. This is the chip's measured behaviour, not a preference about what a summary "ought" to show.

⭐ The per-kind Badge measurement — A is not free

The card's warning was taken literally: every one of the 53 registered field types was rendered through getCellRenderer inside the real chip Badge — same element, same variant, same class string DetailView uses — against the object value { id: 'acct-1', name: 'Acme Corp' }, and the DOM the pill received was counted. The instrument is committed as summaryChip.badgeFitCensus-8464.test.tsx, so the table stays runnable.

38 of 53 fit. They draw plain inline text: Acme Corp for the nameable families; the JSON literal for location / geolocation / address / json / object / composite / record, behind objectui#8481's declared json-literal fence; a value-independent face for password / secret / vector / grid.

15 of 53 do not, in four measured classes:

class types what the pill received
a pill inside a pill select status multiselect radio checkboxes tags SelectCellRenderer's own Badge — one rounded-full node nested inside the chip's own
an avatar composite user two rounded-full nodes, and the initials glued onto the name: ACAcme Corp
an image and no text image avatar signature an <img>, textContent === '' — nothing for the chip to say and nothing for its accessible name
a "No value" face boolean toggle datetime repeater (shared EmptyValue), date (formatDate's own em-dash, objectui#8581) the chip is drawn only after hasCellValue called the value FILLED; a renderer answering "empty" one band later re-opens exactly the cross-band contradiction objectui#8394 closed

⚠️ The avatar row is the one the card warned about: .rounded-full matches two nodes per avatar (Radix Avatar.Root and AvatarFallback). This census counts rather than navigates, so it saw both; the census asserts the count is exactly 2.

The decision this table produces

A for the 38 kinds that fit, with a stated rule for the 15 that do not — the shape the card anticipated, not a hedge.

The stated rule is not a stringifier written for this chip (objectui#8395's option C, "answer-shopping"). The refused kinds fall to coerceToSafeValue, @object-ui/fields' single documented answer to "what text does a cell draw for a value that is not a string". objectui#8596 ruled the option families and user onto exactly that text for an object value, so for 7 of the 15 the chip is byte-equal to its own cell. Following objectui#8596's method: read the existing authority per family, never invent a renderer-side fallback (AGENTS.md #0.1).

⛔ Option B was refused by the card and is not in this change: no object value is dropped. The THE EMPTINESS CLASSIFICATION cases are red for it.

Why the switch fires where it does

On the defect's own signaturedisplay.includes('[object Object]') after the existing four families have had their turn — not on a type guess. Number({}) is NaN, new Date({}) is Invalid and the option lookup falls back to String(val), so all four formatted branches land on the same test rather than needing four of their own. A value the string path already rendered (['a','b']a,b, every scalar) is byte-for-byte untouched, and SCALARS AND SCALAR ARRAYS pins that.

The one cost, stated

A renderer-backed chip carries no aria-label: aria-label overrides content, so it would hide the very value the branch exists to show. Its accessible name is composed from content instead — an sr-only field-name prefix plus the renderer's text — giving the same field: value shape as before, now true. String-path chips keep their aria-label unchanged, so objectui#8394's chipFor navigation and its 'stage: Won' assertion are untouched. Every chip additionally carries data-summary-chip="<field>" as one handle for both paths.

Non-regression: the emptiness classification did not move

objectui#8394 / PR #8457 converged this chip's guard onto hasCellValue as "the non-regressive convergence — it moves whitespace-only strings and nothing else." This change is a display decision and leaves it alone; all four of its answers plus the empty-object case are re-pinned through the chip (THE EMPTINESS CLASSIFICATION, 5 cases). The whole existing packages/plugin-detail/ suite — 1350 tests — passes unchanged.

Caricatures — every pin observed red

⚠️ An assertion never observed to fail has not been tested, so each was run. Read sites were mutated (never a pin), from a committed tree, with a trap … EXIT INT TERM, absolute paths, the mutation proven on disk in both directions (removed-anchor count 0, injected-anchor count ≥ 1, git hash-object ≠ the HEAD blob) plus a line-total gate, and restoration proven by state (git diff HEAD empty) after every leg. Per-test classification comes from vitest's JSON reporter.

Vitest aliases @object-ui/fieldspackages/fields/src (vitest.config.mts:507), so a source mutation is what the test executes — no dist is in the path for these *.test.tsx files.

leg mutation (read site) result
baseline none 26 / 26 pass
1 — render nothing for every object (the fenced B) DetailView.tsx: return null for any object value 13 of 26 RED
2 — render every value through the cell renderer regardless of kind DetailView.tsx: ChipCellRenderer = getCellRenderer(...) unconditionally 19 of 26 RED
3 — refuse the renderer for every kind summaryChipRenderers.ts: chipTakesCellRendererfalse 7 of 26 RED
4 — every type answers what text answers fields/src/index.tsx: getCellRendererTextCellRenderer census: 2 of 5 RED
5 — every type draws the shared No-value affordance fields/src/index.tsx: getCellRendererEmptyValue census: 3 of 5 RED

Both directions the card asked for redden loudly. Leg 2 is the one that refuses "just route everything through the cell renderer": it reds all four formatted families, the percent bar, and the scalars.

Leg 5 initially left THE FIT SIDE IS LOAD-BEARING green — it had no assertion that mutation could break. That is reported rather than papered over: the case was given the discriminating assertion (a fitting kind draws its value, never the data-slot="empty-value" affordance) and re-run, and it is red under leg 5 now.

Verification

what command verdict
new pins pnpm exec vitest run …summaryChip.objectValue-8464.test.tsx …summaryChip.badgeFitCensus-8464.test.tsx Test Files 2 passed (2) · Tests 26 passed (26)
the whole package pnpm exec vitest run packages/plugin-detail/ Tests 1350 passed (1350)
affected packages pnpm exec vitest run packages/plugin-detail/ packages/plugin-calendar/ packages/plugin-gantt/ packages/plugin-kanban/ packages/runner/ packages/app-shell/ Test Files 945 passed (945) · `Tests 8699 passed
types pnpm --filter '@object-ui/plugin-detail^...' build && pnpm --filter @object-ui/plugin-detail type-check exit 0 — and type-check is tsc --noEmit && tsc -p tsconfig.test.json, so the new test files are in a checked program, not excluded
gates check:control-bytes check:doc-fences check:phantom-deps check:unused-deps check:self-import all exit 0
control bytes grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over all six changed files no match
changeset node scripts/check-changeset-presence.mjs ✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) — the gate's own verdict line. ⚠️ skip-changeset is a phantom label here; this is a user-visible fix, so it declares a real patch, not an empty frontmatter
governed surface node scripts/check-governed-queue-guard.mjs --test <the 6 paths> ✅ NOT GOVERNED — 6 path(s) checked against 5 governed surface(s); none matched.

Lint, narrowed and the narrowing measured. pnpm exec eslint <the 4 code files> --format json → exit 0, 4 files (count read from the JSON output, not asserted), 0 errors, 82 warnings — all @typescript-eslint/no-explicit-any and two pre-existing react-hooks warnings that DetailView.tsx already carried. The narrowing is a measurement rather than a skipped run because type-aware linting is not enabled: eslint.config.js has zero project / projectService entries, so nothing in this diff can move the verdict on a file it did not touch. CI's repo-wide eslint . is the run that covers the population, and .github/workflows/lint.yml deliberately sets no --max-warnings, so warnings are not a failure there either. ⛔ --no-inline-config was not used — that is an objectstack convention and manufactures errors this repo's CI does not have.

Scope

⚠️ This card's own dedup had never been run (its reporter was rate-limited and REST answered 403), so it was run here rather than inherited as a clean bill. Channel: MCP search_issues — repo-scoped REST /search/issues is refused in this container ("sessions are bound to their configured repositories"), so the suggested query was run through MCP. Its control fired: a query for this defect returned #8464 itself as the top hit, so the zero is real and not one of this tool's false zeros. No duplicate. #4054 is adjacent, not a duplicate — see the limit below.

Two defects found on these lines and filed rather than ridden along:

⚠️ What this change does not measure

Pixels. happy-dom reports clientWidth: 0 and never fires container-size effects, so the census measures what DOM the pill hosts, not what it clips. That half is #4054 — "a record:highlights chip clips multi-element cell renderers with NO ellipsis" — which says in its own words that "a DOM-only probe reports this surface healthy" and needs getBoundingClientRect() plus a screenshot. Its analysis also records that lookup / user / file carry their own truncate and are unaffected, and names the MapPin-plus-text row — this repo's location — as a candidate. This change makes location reachable in a summary chip, so it is now in #4054's population; the structural verdicts here stand either way.

Collisions

git ls-remote --heads origin 'gh-readonly-queue/*' at branch time listed PRs #8646, #8668, #8706, #8707, #8711. None touches packages/plugin-detail/** (file lists read per PR). PR #8685 (plugin-detail) had landed, so the package was free.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

os-justin and others added 4 commits September 9, 2026 01:31
…lue through its own cell renderer

`effectiveSummaryFields`' chip displayed `String(val)` with four families
special-cased, so an object-valued summary field — an expanded lookup payload,
a location, an address — printed the literal `[object Object]` next to the page
title, and its accessible name (built from the same string) said it too.
Reachable both ways: `schema.summaryFields` is author-declared and never
filtered by type, and the auto-detection can hand the status slot to a field
whose stored value is not a scalar.

Which question this chip asks was measured, not argued: it already prints
`$1,235` for a stored `1234.5`, `Mar 4, 2026` for `'2026-03-04'` and
`Closed Won` for `'won'` — the seen face, never the stored one — so the display
authority for a kind it does not format is that kind's own cell renderer, the
way `HeaderHighlight` reads it one band below.

That route is not free. A Badge is a much smaller surface than a cell: with all
53 registered types rendered through `getCellRenderer` inside the real chip
Badge against an object value, 15 draw a nested pill, an avatar composite, a
bare `<img>` with no text, or a "No value" face for a value `hasCellValue` had
just called filled. Those kinds are named with the measurement in
`summaryChipRenderers.ts` and take `coerceToSafeValue`, this repo's single
answer to the same question and byte-equal to what seven of them print in their
own cell (objectui#8596). Nothing here invents a chip-local stringifier.

The switch fires on the defect's own signature — the string path having
produced the placeholder — so every value that already rendered, scalars and
scalar arrays included, is byte-for-byte untouched, and the emptiness
classification objectui#8394 converged onto `hasCellValue` is re-pinned rather
than moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ure can break

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…lar value, and add the changeset

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin tests labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3482.2 KB 3512.7 KB
Main entry chunk (gzip) 144.0 KB 350 KB
Entry file index-C8WJ7_-H.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.96KB 61.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.39KB 65.32KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.16KB 33.47KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.18KB 56.62KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin marked this pull request as ready for review September 9, 2026 02:00
@os-justin
os-justin added this pull request to the merge queue Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review — accepted, flipped out of draft, auto-merge armed. 33/33 green.

The thing that makes this PR right is that arm A was not taken on the argument that it was the obvious one. Which question the chip asks was settled by re-deriving the chip's existing behaviour: it already prints $1,235 for a stored 1234.5, Mar 4, 2026 for '2026-03-04', Closed Won for 'won' — the seen face every time — so the display authority is the field's own cell renderer, and objectui#8395's "seen vs stored" fork was answered from the code rather than from taste.

Three things I re-measured rather than read.

1. The fallback is not the defect in another costume. coerceToSafeValue resolves an object to name || label || externalId || id || _id || '[Object]' — never [object Object]. So the 15 refused kinds get a real answer, and it is @object-ui/fields' existing one rather than a chip-local stringifier. objectui#8596 already ruled 7 of those 15 onto exactly that text, so for those the chip is byte-equal to its own cell. That is what keeps this inside AGENTS.md #0.1 instead of shopping for a third answer.

2. The census closes the trap it names. A matrix pin over an empty set passes, and this one guards against that and meta-tests the guard:

it('THE CENSUS — the guard refuses a short or EMPTY set (a census over nothing passes)')
  expect(() => assertCensusComplete([], REGISTERED_TYPE_COUNT, 'census')).toThrowError(…)

plus no-duplicate-types, measured === 53 ("every row was rendered, not skipped"), and fitting.length === 38. The ⭐ THE SET MATCHES THE MEASUREMENT case derives CHIP_UNFIT_RENDERER_TYPES from the rendered table rather than from memory, so the constant cannot drift away from what the pills actually did.

3. The gate is the defect's own signature, and it sits in the right place. display.includes('[object Object]') is tested after the four formatted branches, so Number({})NaN, new Date({}) → Invalid and the option-lookup fallback all funnel into one test instead of four — and a value the string path already renders (['a','b']a,b, every scalar) is untouched byte-for-byte.

The aria-label removal is correct for a reason worth stating on the record: an aria-label on the Badge would override the rendered element's own accessible content — it would hide the very value the renderer branch exists to expose. Composing the same field: value name from an sr-only prefix plus content is the only spelling that keeps both the name and the value. And data-summary-chip going onto all three branches is what lets a test address a chip without depending on which branch drew it.

Reported honestly, and it is the part I trust most: leg 5 first left THE FIT SIDE IS LOAD-BEARING green — an assertion never observed to fail. It was given a discriminating assertion (a fitting kind draws its value, never the data-slot=empty-value affordance) and re-run red. A dev that reports its own green leg as a defect is a dev whose red legs mean something.

⚠️ One finding this review turned up, filed rather than sent back — it cannot be closed inside this PR. The census population is frozen at 53 and the default is permissive. REGISTERED_TYPE_COUNT and CENSUS are hard-coded, and nothing reconciles them against the live registry — getCellRenderer dispatches on fieldRegistry plus a standardMap that is a function-local const (packages/fields/src/index.tsx:2608) and therefore not enumerable from outside. So when a 54th type is registered: the census still passes at 53, the new type is absent from CHIP_UNFIT_RENDERER_TYPES, and chipTakesCellRenderer returns true by default — routing it to its own renderer unmeasured. The drift is silent and in the unsafe direction: if that renderer draws a nested pill, a bare <img>, or a "No value" face, all four defects this census enumerates return with nothing red. Closing it means exposing the standard map from @object-ui/fields, which is another package's surface. Filed as a follow-up; nothing to do here.

On the open question raised in the report — the dispatch text does read as self-contradictory (no model identifier in a commit message, then a mandated Co-Authored-By trailer naming one). Reading A is right and is now the standing ruling: the trailer is the named exception, spelled out verbatim by the session attribution notice; the prohibition covers every surface it does not name — PR title and body, code comments, changesets, and any other pushed artefact. Nothing in this PR carries one outside the trailer.

The two by-products (#8728, the percent chip stating two different percentages; #8729, the chip naming its field by the raw column name) were correctly kept out: the second one moves an instrument objectui#8394 owns.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation plugin tests

Projects

None yet

1 participant